package net.aiion.weave.impl.xml;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

import javax.xml.namespace.QName;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.Comment;
import javax.xml.stream.events.DTD;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.Namespace;
import javax.xml.stream.events.ProcessingInstruction;
import javax.xml.stream.events.StartDocument;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import net.aiion.weave.spi.resources.Resource;
import net.aiion.weave.spi.xml.XmlEventFactory;
import net.aiion.weave.spi.xml.XmlException;
import net.aiion.weave.spi.xml.events.AttributeXmlEvent;
import net.aiion.weave.spi.xml.events.NamespaceXmlEvent;
import net.aiion.weave.spi.xml.events.XmlEvent;

public class XmlReaderImpl extends AbstractXmlSource
{
    private final XmlEventFactory xmlEventFactory;

    private final Resource resource;
    private XMLEventReader xmlEventReader;

    public XmlReaderImpl(final Resource aResource) {
        xmlEventFactory = XmlEventFactory.getInstance();
        resource = aResource;
    }

    @Override
    public void startSourcing() {
        try (InputStream is = resource.openStream()) {
            final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
            xmlInputFactory.setProperty("javax.xml.stream.supportDTD", false);
            xmlEventReader = xmlInputFactory.createXMLEventReader(is);

            dispatchXmlEvent(XmlEventFactory.getInstance().createSource(resource.getName()));
            while (xmlEventReader.hasNext()) {
                dispatchXmlEvent(adaptXMLEvent(xmlEventReader.nextEvent()));
            }
        } catch (final XMLStreamException | FactoryConfigurationError e) {
            throw new XmlException(e);
        } catch (final IOException e) {
            throw new XmlException(e);
        } finally {
            try {
                if (xmlEventReader != null) {
                    xmlEventReader.close();
                }
            } catch (final XMLStreamException e) {
                throw new XmlException(e);
            }
        }
    }

    private XmlEvent adaptXMLEvent(final XMLEvent aXMLEvent) {
        final XmlEvent xmlEvent;
        switch (aXMLEvent.getEventType()) {
        case XMLStreamConstants.START_DOCUMENT:
            xmlEvent = adaptStartDocument((StartDocument) aXMLEvent);
            break;
        case XMLStreamConstants.DTD:
            xmlEvent = adaptDTD((DTD) aXMLEvent);
            break;
        case XMLStreamConstants.COMMENT:
            xmlEvent = adaptComment((Comment) aXMLEvent);
            break;
        case XMLStreamConstants.PROCESSING_INSTRUCTION:
            xmlEvent = adaptProcessingInstruction((ProcessingInstruction) aXMLEvent);
            break;
        case XMLStreamConstants.START_ELEMENT:
            xmlEvent = adaptStartElement((StartElement) aXMLEvent);
            break;
        case XMLStreamConstants.CHARACTERS:
            xmlEvent = adaptCharacters((Characters) aXMLEvent);
            break;
        case XMLStreamConstants.END_ELEMENT:
            xmlEvent = adaptEndElement((EndElement) aXMLEvent);
            break;
        case XMLStreamConstants.END_DOCUMENT:
            xmlEvent = adaptEndDocument();
            break;
        default:
            throw new XmlException("Unsupported event type " + aXMLEvent.getEventType());
        }
        return xmlEvent;
    }

    private XmlEvent adaptStartDocument(final StartDocument aStartDocument) {
        return xmlEventFactory.createDocumentStart(aStartDocument.getVersion(),
            aStartDocument.getCharacterEncodingScheme(), aStartDocument.isStandalone());
    }

    private XmlEvent adaptDTD(final DTD aDTD) {
        return xmlEventFactory.createDocumentTypeDefinition(aDTD.getDocumentTypeDeclaration());
    }

    private XmlEvent adaptComment(final Comment aComment) {
        return xmlEventFactory.createComment(aComment.getText());
    }

    private XmlEvent adaptProcessingInstruction(final ProcessingInstruction aProcessingInstruction) {
        return xmlEventFactory.createProcessingInstruction(aProcessingInstruction.getTarget(),
            aProcessingInstruction.getData());
    }

    private XmlEvent adaptStartElement(final StartElement aStartElement) {
        final QName startElementQName = aStartElement.getName();

        final Collection<AttributeXmlEvent> xmlAttributes = new ArrayList<>();
        for (final Iterator<?> it = aStartElement.getAttributes(); it.hasNext();) {
            final Attribute attribute = (Attribute) it.next();
            final QName attributeName = attribute.getName();
            xmlAttributes.add(xmlEventFactory.createAttribute(attributeName.getPrefix(),
                attributeName.getNamespaceURI(), attributeName.getLocalPart(), attribute.getValue()));
        }

        final Collection<NamespaceXmlEvent> xmlNamespaces = new ArrayList<>();
        for (final Iterator<?> it = aStartElement.getNamespaces(); it.hasNext();) {
            final Namespace namespace = (Namespace) it.next();
            xmlNamespaces.add(xmlEventFactory.createNamespace(namespace.getPrefix(), namespace.getNamespaceURI()));
        }

        return xmlEventFactory.createElementStart(startElementQName.getPrefix(), startElementQName.getNamespaceURI(),
            startElementQName.getLocalPart(), xmlAttributes, xmlNamespaces);
    }

    private XmlEvent adaptCharacters(final Characters aCharacters) {
        return xmlEventFactory.createCharacters(aCharacters.getData());
    }

    private XmlEvent adaptEndElement(final EndElement aEndElement) {
        final QName endElementName = aEndElement.getName();
        return xmlEventFactory.createElementEnd(endElementName.getPrefix(), endElementName.getNamespaceURI(),
            endElementName.getLocalPart());
    }

    private XmlEvent adaptEndDocument() {
        return xmlEventFactory.createDocumentEnd();
    }
}
